﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AzureGames.Common;
using AzureGames.Data;
using AzureGames.Model;
using AzureGames.Domain.Contracts;
using AzureGames.Domain.Models;
using SignalR.Hubs;
using SignalR;
using System.Threading.Tasks;

namespace AzureGames.Domain.Services
{
    [HubName("notificationService")]
    /// <summary>
    /// Service used to manage the communication between the client browsers and the data 
    /// repository notifications (Queues)
    /// This is a signalr hub
    /// </summary>
    /// <remarks>this will be instantiated as a singleton (by the global startup class).  The notification repository
    /// it depends upon is a singleton also (built by unity)
    /// </remarks>
    public sealed class NotificationService : Hub, IDisconnect
    {
        public static readonly NotificationService Instance = new NotificationService();
        /// <summary>
        /// dependents (MVC sessions) observing the subject (_notificationRepository) via the observer (NotificationService)
        /// </summary>
        /// <remarks>the messages are posted to the current sessions (string) and removed as they are delivered to the client's browser (guid=playerid)</remarks>
        private static readonly Dictionary<NotificationSession, List<NotificationMessage>> Sessions = new Dictionary<NotificationSession, List<NotificationMessage>>();
        private static INotificationRepository NotificationRepository;
        private static IBoardRepository BoardRepository;

        public NotificationService()
        {

        }

        /// <summary>
        /// "Factory" to create a parameterised singleton of this class
        /// </summary>
        /// <param name="notificationRepository">singleton notificationRepository</param>
        /// <returns></returns>
        public void IntializeNotificationService(INotificationRepository notificationRepository, IBoardRepository boardRepository)
        {
            NotificationRepository = notificationRepository;
            BoardRepository = boardRepository;
            NotificationRepository.NotifyEvent += new NotifyEventHandler(NotificationRepository_NotifyEvent);
        }


        public void AddSession(string playerID)
        {
            // if the connection is not already added, add it
            var connection = Sessions.Keys.FirstOrDefault(s => s.SessionID == Context.ConnectionId);

            if (connection == null)
                Sessions[new NotificationSession { PlayerID = playerID, SessionID = Context.ConnectionId }] = new List<NotificationMessage>();
            else
                if (connection.PlayerID != playerID)
                    connection.PlayerID = playerID;

            if (playerID != Guid.Empty.ToString())
                NotificationRepository.Attach(playerID);
        }


        private void RemoveSession(string sessionID)
        {
            var session = Sessions.Keys.FirstOrDefault(s => s.SessionID == sessionID);

            if (session != null)
            {
                if (session.PlayerID != Guid.Empty.ToString())
                    NotificationRepository.Detach(session.PlayerID);

                Sessions.Remove(session);
            }

        }


        /// <summary>
        /// called when a notification message relavant to this observer is avilable for delivery
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        bool NotificationRepository_NotifyEvent(INotificationRepository sender, NotifyEventArgs args)
        {
            if (args.Message.PlayerID == Guid.Empty.ToString())
            {
                if (args.Message.BoardID == Guid.Empty)
                {
                    // general notification so put in everyones queue
                    Sessions.ForEach(session =>
                    {
                        lock (session.Key)
                        {
                            session.Value.Add(new NotificationMessage(args.Message));
                            GetAndSendNotifications(session.Key.SessionID);
                        }

                    });
                }
                else
                {
                    // notify all players of the game                    
                    List<string> players = BoardRepository.GetGameBoard(args.Message.BoardID).PlayerIDs;

                    Sessions.Where(session => players.Contains(session.Key.PlayerID)).ForEach(session =>
                    {
                        lock (session.Key)
                        {
                            session.Value.Add(new NotificationMessage(args.Message));
                            GetAndSendNotifications(session.Key.SessionID);
                        }
                    });
                }
            }
            else
            {
                Sessions.Where(session => session.Key.PlayerID == args.Message.PlayerID).ForEach(session =>
                {
                    lock (session.Key)
                    {
                        session.Value.Add(new NotificationMessage(args.Message));
                        GetAndSendNotifications(session.Key.SessionID);
                    }
                });
            }

            // TODO: this could be true when websockets are enabled
            return false;
        }

        private void GetAndSendNotifications(string notificationSessionId)
        {
            var list = GetNotifications(notificationSessionId, 5); //arbitrary number at the moment, need to decide on what the value should be, also get rid of the magic number!
            list.ForEach(n => Send(notificationSessionId, n));
        }



        public static IEnumerable<NotificationMessage> GetNotifications(string sessionID, int maxRecords)
        {
            IEnumerable<NotificationMessage> response;

            // lock on the key so no changes can be made to inner list of messages
            lock (Sessions.First(session => session.Key.SessionID == sessionID).Key)
            {
                var listOfMessages = Sessions.First(session => session.Key.SessionID == sessionID).Value;

                // remove the messages from the list,
                // mark as delivered
                // and return new notification messages
                response = listOfMessages.Take(maxRecords).ToArray()
                    //.ForEach(message => _notificationRepository.UpdateState(message.ID, Notification.STATE.Delivered))
                    .ForEach(message => listOfMessages.Remove(message));
                //.Select(n => new NotificationMessage(n));
                //.ForEach(message => response.Add(message));

                // if player specific then update the repository to say it was delivered
                response.Where(n => n.PlayerID != Guid.Empty.ToString())
                    .ForEach(n => NotificationRepository.UpdateState(n.ID, Notification.STATE.Delivered));
                //.ForEach(n => listOfMessages.Remove(n));
            }

            return response;
        }

        #region signalr methods
        /// <summary>
        /// Retrieve the signalr context
        /// </summary>
        /// <returns></returns>
        private IHubContext GetContext()
        {
            return GlobalHost.ConnectionManager.GetHubContext<NotificationService>();
        }
        

        private void Send(string notificationSessionId, NotificationMessage message)
        {
            // specific client 
            GetContext().Clients[notificationSessionId].sendMessage(message);
        }

        public void PostNotification(Notification notification)
        {
            NotificationRepository.PostNotification(notification);            
        }
        
        #endregion

        #region IDisconnect Members

        public Task Disconnect()
        {
            return Task.Factory.StartNew(() =>
            {
                RemoveSession(Context.ConnectionId);
            });
        }

        //public Task Disconnect()
        //{
        //    return new Task(() => RemoveSession(Context.ConnectionId));
        //}

        #endregion

    }
}
